/*
 * Copyright 2010, 2011 by the University of Richmond, Richmond, VA 23173
 * Written by Eric F. Palmer eric@ericfpalmer.com || epalmer@richmond.edu
 * And other contributors
 * http://www.richmond.edu/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Project files reside at http://code.google.com/p/files-2-exist-db
 */

package edu.richmond.is.webservices.files2exist;

import edu.richmond.is.webservices.f2eJmxAgent;
import edu.richmond.is.webservices.files2exist.AbstractTransform;
import edu.richmond.is.webservices.files2exist.ConfigConstants;
import edu.richmond.is.webservices.files2exist.Main;
import edu.richmond.is.webservices.util.FileChange;
import edu.richmond.is.webservices.util.FilesInFolderList;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.net.URISyntaxException;
import org.apache.log4j.Logger;
import org.apache.log4j.MDC;
import org.apache.log4j.PropertyConfigurator;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;

/**
 *
 * @author epalmer
 */
public class Main {
     // imported into IDEA IDE
     //Variables below that are "default" level visibility are set in that manner
     //so that the Files2ExistShutdown class can access them.
    static Logger logger = Logger.getLogger(ConfigConstants.LOGGER_GENERAL);
    private static Logger heartbeatLogger = Logger.getLogger(ConfigConstants.LOGGER_HEARTBEAT);
    private static Logger transferLogger = Logger.getLogger(ConfigConstants.LOGGER_TRANSFER);
    static boolean stopAppSignal         = false;
    static boolean shutdownSignal        = false;

    // jmx properties
    static public long masterLoopCount   = 0;
    static public long fileTransferCount = 0;
    static public long startTime         = System.currentTimeMillis();
    static public long charsTransferred  = 0;
    static public long lastFileSize      = 0;
    static public long minFileChars = Long.MAX_VALUE;
    static public long maxFileChars = 0;
    
    //
    // Dynamic plugin classloader
    //
    private static ClassLoader pluginClassloader = null;

    public static void main(String[] args) {
    	
        String cmdLineConfigFileLoc = null;
        PropertyConfigurator.configure(ConfigConstants.DEFAULT_LOG4J_FILE);
        loadHostname();

        Runtime.getRuntime().addShutdownHook(new Files2ExistShutdown());

        logger.error(
            "\n============================================\n"+
            "Starting Application\n" +
            "============================================\n");

        logger.info("starting JMX SImpleAgent");
        @SuppressWarnings("unused")
		f2eJmxAgent agent = new f2eJmxAgent();

        String pluginDir = ConfigConstants.DEFAULT_PLUGIN_DIR;
        String mimeTypesFileLoc = ConfigConstants.DEFAULT_MIME_TYPES;
        //Check for a command line argument that begins with -Dconfig
        if(args.length > 0){
            for(int i=0; i<args.length; i++){
                
                if(args[i].startsWith(ConfigConstants.CMD_CONFIG_PROP)){
                    cmdLineConfigFileLoc = args[i].substring(9);
                    logger.debug("Loading configuration from command line supplied file : " + cmdLineConfigFileLoc);
                } else if(args[i].startsWith(ConfigConstants.CMD_PLUGINDIR_PROP)) {
                	pluginDir = args[i].substring(ConfigConstants.CMD_PLUGINDIR_PROP.length());
                	logger.debug("Loading plugins from dir:" + pluginDir);
                } else if(args[i].startsWith(ConfigConstants.CMD_MIME_TYPES)) {
                	mimeTypesFileLoc = args[i].substring(ConfigConstants.CMD_PLUGINDIR_PROP.length());
                	logger.debug("Overriding default mime-types loc:" + mimeTypesFileLoc);
                }
            }
        }

        createPluginClassloader(pluginDir);
        
        MimeTypeMapper mime = null;
        try { 
        	mime = new MimeTypeMapper(mimeTypesFileLoc);
        } catch (IOException e) {
			e.printStackTrace();
			shutdownApplication(-2);
		}
        AppConfig configObj = new AppConfig(cmdLineConfigFileLoc);
        List<Folder2ExistSettings> dirListObj = configObj.getFolderSettingsArrayList();
        int fileFolderReaderCounter     = 0;

        do {
          ++Main.masterLoopCount;
          try{
            // we process one folder at a time till all the files were initially
            // detected have been written to exist and deleted
            for (Folder2ExistSettings folderSettings: dirListObj) {
                
                logger.trace("Looping through dirListObj");
                ++fileFolderReaderCounter;

                //Heartbeat
                if (fileFolderReaderCounter >= configObj.getHeartbeatLoopCounter()) {
                    heartbeatLogger.info("HEARTBEAT");
                    logger.info("HEARTBEAT");
                    fileFolderReaderCounter = 0;
                }


                // get the list of files in the current folder
                // this should be tolerant of empty fileList

                //this now does a recursive lookup
                FilesInFolderList fileList = new FilesInFolderList(folderSettings.getFolderName(), folderSettings.isRecurse(), folderSettings.getFilePattern());
                if (fileList.getFileListSize() > 0) {
                    List<File> fileArrayList = fileList.getfileArrayList();
                    List<FileChange> fileChangeArrayList = new ArrayList<FileChange>();

                    for (File tempFile : fileArrayList) {
                        fileChangeArrayList.add(new FileChange(tempFile.getCanonicalFile().toString()));
                    }

                    fileList = null;
                    fileArrayList = null;

                    // might as well wait the require time before checking if files have stopped changing
                    Thread.sleep(configObj.getApplicationSleepTime());


                   // we keep looking at a list of files in a folder and moving them to
                   // eXist-Db when they are no longer changing file size
                   // we stop only when there are no more files to process

                   // note that files can be added to the folder while we are processing this list
                   // this list does not grow but we will come back and look at the contents of this folder again
                   //TODO enable detection of file types (mime type) 2nd argument to restPut....

                    while(fileChangeArrayList.size() > 0){
                         int fileIndex = 0;

                         if (!fileChangeArrayList.get(fileIndex).isFileSizeChanging()) {
                              //TODO get the extraPath stuff in here
                              if (folderSettings.getTransformName() != null && !(folderSettings.getTransformName().trim().length() == 0)){
                                callTransform(folderSettings, fileChangeArrayList.get(fileIndex).getCanonicalFile().toString());
                              }

                              transferLogger.info("Writing File to exist-Db: " + folderSettings.getHostName() + " " + fileChangeArrayList.get(fileIndex).getCanonicalFile().toString());
                              logger.info("Writing File to exist-Db: " + folderSettings.getHostName() + " " + fileChangeArrayList.get(fileIndex).getCanonicalFile().toString());
                              lastFileSize =  fileChangeArrayList.get(fileIndex).length();
                              ExistRest restPut = new ExistRest(folderSettings.getHostName(), folderSettings.getHostPort(), folderSettings.isUseSsl());

                              String extraExistCollectionPath = FilesInFolderList.getExtraPath(folderSettings.getFolderName(), (File) fileChangeArrayList.get(fileIndex));
                              String existdbFilePath = folderSettings.getExistCollection() + extraExistCollectionPath + "/"
                                                       + fileChangeArrayList.get(fileIndex).getName();

                              logger.debug("eXist-Db filePath: " + existdbFilePath);

                              try {
                                  logger.debug("u/p: *"+folderSettings.getExistUsername() + "/" + folderSettings.getExistPassword() + "*");
                                  logger.debug("path: " + existdbFilePath);


                                  logger.debug("extraPath: " + extraExistCollectionPath);
                                  
                                  File targetFile = fileChangeArrayList.get(fileIndex);
                                  String mimeType = mime.findMimeMapping(targetFile);
                                  if(!folderSettings.isAllowBinary()) {
                                	 if(!mime.isXMLMimeType(mimeType)) {
                                		 throw new Exception("Non-xml mime-type encountered when binary not allowed: " + targetFile + ":" + mimeType);
                                	 }
                                  }
                                  
                                  restPut.restPut(targetFile,
                                        mimeType,
                                        existdbFilePath,
                                        folderSettings.getExistUsername(),
                                        folderSettings.getExistPassword());
                              } catch (URISyntaxException use) {
                                   logger.fatal(getLogExceptionMsg(use, "uri: "+restPut.getLastUri() +
                                           "\nSourceFolder: " + folderSettings.getFolderName()));
                              } catch(MimeTypeMapper.MimeMappingNotFoundException mmnfe) {
                            	  logger.fatal(getLogExceptionMsg(mmnfe, "Invalid file extension."));
                            	  shutdownApplication(3);
                              } catch(Exception e){
                                   logger.fatal(getLogExceptionMsg(e, "Caught unknown exception while attempting file transfer to eXist-Db\n" +
                                           "\nSourceFolder: "    + folderSettings.getFolderName() +
                                           "\nExistHost: "       + folderSettings.getHostName() +
                                           "\nExistCollection: " + folderSettings.getExistCollection()));
                                   shutdownApplication(1);
                              }
                              ++fileTransferCount;
                              charsTransferred += lastFileSize;
                              if (lastFileSize < minFileChars) {
                                  minFileChars = lastFileSize;
                              }
                              if (lastFileSize > maxFileChars) {
                                  maxFileChars = lastFileSize;
                              }

                              restPut = null;

                              try {
                                   fileChangeArrayList.get(fileIndex).delete();
                                   logger.debug("File deleted: "+fileChangeArrayList.get(fileIndex).getName());

                                   if (fileChangeArrayList.get(fileIndex).exists()) {
                                        logger.warn("*** file still exists***");
                                        throw new Exception("Unable to delete file from file system: "+fileChangeArrayList.get(fileIndex).getCanonicalPath());
                                   }
                              } catch (Exception e) {
                                   logger.fatal(getLogExceptionMsg(e, "Unable to delete file from file system: " + fileChangeArrayList.get(fileIndex).getCanonicalPath() ));
                                   shutdownApplication(1);
                              }

                              //logger.debug("Removing File from Array List: " + fileChangeArrayList.get(fileIndex).getCanonicalFile().toString());
                              fileChangeArrayList.remove(fileIndex);
                         } else {
                              // do nothing - could add sleeping some
                              // sleep for a second and let the file settle out
                              // this will slow down the whole process but
                             // TODO add this as a config setting
                              Thread.sleep(500);
                              //logger.debug("\n****** File may still be growing: " + fileChangeArrayList.get(fileIndex).getCanonicalFile().toString() + " ******\n");
                         }//endif
                    }//endwhile
                    fileChangeArrayList = null;
                }//endif
            } // end foreach loop

            Thread.sleep(configObj.getApplicationSleepTime());
          }catch(IOException ioe){
               logger.fatal(getLogExceptionMsg(ioe, ""));
               shutdownApplication(1);
          }catch(InterruptedException ie){
               logger.fatal(getLogExceptionMsg(ie, ""));
          }catch(Exception e){
               logger.fatal(getLogExceptionMsg(e, ""));
               shutdownApplication(1);
          }

     }while (!stopAppSignal);

     shutdownSignal = true;
     logger.error("SHUTDOWN :: File transfers complete.");
    }
    
    private static void createPluginClassloader(String pluginDir) {
    	//
    	// Create a classloader which derives from the
    	// parent and appends the jar files in the plugin'
    	// directory
    	//
    	try {
    		File dir = new File(pluginDir);
    		if(!dir.exists() || !dir.isDirectory()) {
    			logger.debug("Plugin dir does not exist:" + pluginDir);
    			pluginClassloader = new URLClassLoader(new URL[]{}, Main.class.getClassLoader());
    		} else {	
    			File[] files = dir.listFiles();
    			ArrayList<URL> urls = new ArrayList<URL>(files.length);
    			for(File f: files) {
    				urls.add(f.getAbsoluteFile().toURI().toURL());
    			}
    			pluginClassloader = new URLClassLoader(urls.toArray(new URL[urls.size()]), Main.class.getClassLoader());
    		}
    	} catch(MalformedURLException e) {
    		logger.error("Caught unexpected error loading pluginClassloader from directory " + pluginDir + ":" +  e.toString());
    	}
		
	}
    
    private static void callTransform(Folder2ExistSettings folderSettings, String fileName) throws Exception {
        // calls the transform for the indicated file

        logger.debug("callTransform called for TransformName: " + folderSettings.getTransformName());
        AbstractTransform myT;

		try {
			myT = (AbstractTransform) Class.forName(folderSettings.getTransformName(), true, pluginClassloader).newInstance();
		} catch (Exception e) {
			throw new Exception("Transform failed to instanciate", e);
		}

        myT.setFolderName(folderSettings.getFolderName());
        myT.setHostName(folderSettings.getHostName());
        myT.setHostPort(folderSettings.getHostPort());
        myT.setExistCollection(folderSettings.getExistCollection());

        myT.setExistPathCollectionPrefix("/unknown");
        myT.setSslExistSite(folderSettings.isUseSsl());
        myT.setExistUsername(folderSettings.getExistUsername());
        myT.setExistPassword(folderSettings.getExistPassword());
        myT.setFileName(fileName);
            myT.setTransformType(folderSettings.getTransformType());

        logger.debug(myT.toString());
        logger.debug("transforming content");
        boolean statusOfTransform = myT.transform();
        if(statusOfTransform != true) {
        	throw new Exception("Transform failed");
        }
    }


     protected static String getLogExceptionMsg(Exception e, String msg){
        String returnMsg =
                "\n\n------- EXCEPTION BEGIN -------\n" +
                msg + "\n" +
                "MESSAGE: " +e.getMessage() + "\n" +
                "STACKTR: " + getStackTracePrintable(e) + "\n" +
                "------- EXCEPTION END   -------\n" +
                "*******************************";
        return returnMsg;
     }

     private static String getStackTracePrintable(Exception e){
        Writer writer = new StringWriter();
        PrintWriter printWriter = new PrintWriter(writer);
        e.printStackTrace(printWriter);
        return writer.toString();   
     }
     protected static void shutdownApplication (int status){
          shutdownSignal = true;
          System.exit(status);
     }
     //Set the host name in MDC so that log4j can print it out
     //in the emailed logs (via %X{host} ). The word "host" is the
     //key that is being used here
     protected static void loadHostname(){
          String hostname = "";
          try {
               InetAddress addr = InetAddress.getLocalHost();
               // Get hostname
               hostname = addr.getHostName();
          } catch (Exception e) {}

          MDC.put("host", hostname);
     }
}
